Begin rewrite to typescript and only React 19 compatibility#218
Conversation
ibacher
left a comment
There was a problem hiding this comment.
I like API here a lot. A few minor points on Typescript stuff, but it feels pretty elegant. I actually really like the idea of passing the functions in rather than the whole of react-dom/client.
I know this is early stage, but is it part of the goal of the rewrite to drop support for things like the SingleSpaProps context and SingleSpaRoot? I'm actually somewhat in favour of doing so (with OpenMRS, we already have a wrapper that can provide similar facilities, so this allows me to opt-out of stuff I don't need in the React tree).
| createRoot: typeof createRoot; | ||
| hydrateRoot: typeof hydrateRoot; | ||
| rootOptions?: RootOptions; | ||
| createReactNode: ((props) => ReactNode) | ((props) => Promise<ReactNode>); |
There was a problem hiding this comment.
To help with client code, maybe this signature would work:
| createReactNode: ((props) => ReactNode) | ((props) => Promise<ReactNode>); | |
| createReactNode<T = any>(props: T): ReactNode | Promise<ReactNode>; |
It should be basically equivalent with the addition that if there is a type for the props, the createReactNode() implementation can be fully typed.
| export interface SingleSpaReactOpts extends DomElementGetterOpts { | ||
| createRoot: typeof createRoot; | ||
| hydrateRoot: typeof hydrateRoot; |
There was a problem hiding this comment.
Slightly more complicated, but something like this:
type AtLeastOne<T> = { [Key in keyof T]: Pick<T, Key> }[keyof T];
export type SingleSpaReactOpts = DomElementGetterOpts & AtLeastOne<{
createRoot: typeof createRoot;
hydrateRoot: typeof hydrateRoot;
}> & {
rootOptions?: RootOptions;
createReactNode<T = any>(props: T): ReactNode | Promise<ReactNode>
}Actually matches the requirements on line 27 and makes it possible (in TS) to pass in either createRoot() or hydrateRoot() or both.
There was a problem hiding this comment.
For the "both" case, it may be nice to have something like the old renderType property since that allows isomorphic app definitions, but it's probably not required.
I'm leaning against SingleSpaContext existing within single-spa-react, instead encouraging the following code in user-space: export const SingleSpaContext = createContext();
const { init, mount, unmount } = singleSpaReact({
createRoot,
hydrateRoot,
rootOptions:,
createReactNode(props) {
return (
<SingleSpaContext value={props}>
<App />
</SingleSpaContext>
);
}
});I'm also leaning against SingleSpaRoot. My understanding is that |
|
Update on SingleSpaRoot: the react docs indicate that https://react.dev/reference/react-dom/client/createRoot#root-render-caveats |
9c54a9b to
5e277f8
Compare
|
|
||
| if (typeof configProp === "function") { | ||
| let aborted = false; | ||
| const configPromise = config(); |
There was a problem hiding this comment.
| const configPromise = config(); | |
| const configPromise = configProp(); |
There was a problem hiding this comment.
good find, updated and added a test
| if (typeof configProp === "function") { | ||
| let aborted = false; | ||
| const configPromise = config(); | ||
| if (!(configPromise instanceof Promise)) { |
There was a problem hiding this comment.
Would it make sense to do something like const configPromise = Promise.resolve(configProp()); to allow both synchronous and asynchronous functions here?
There was a problem hiding this comment.
I don't see a reason why a synchronous function would be needed, since you could instead just provide configProp directly. There are no arguments passed to configProp when it's a function, so synchronous function has no use case from what I can tell
| async unmount(props: AppProps & ExtraProps) { | ||
| instances[props.name].unmount(); | ||
| const domElement = chooseDomElementGetter(opts, props)(); | ||
| domElement.remove(); |
There was a problem hiding this comment.
It makes sense to remove the dom element assuming it was created during the mount, but at least in OpenMRS, we actually rely on pre-creating the dom nodes that single-spa apps render into. This is mostly to support things like a global layout (we have a global header, a global nav, and a global pop-in called "the workspace"; these things need to "wrap-around" arbitrary MFEs), so removing those domElements on unmount() could result in the app misbehaving when the app is remounted, since the dom element would now be in an arbitrary location.
There was a problem hiding this comment.
Good point, I believe that at Canopy they also are not removing the container elements. Also see this line within single-spa-layout, which removes the dom elements at the layout level rather than framework adapter level.
I think the best solution may be to remove the domElement only if it was created by dom-element-getter-helpers, like you suggested. I'll update the code for this.
Totally on-board with that.
Makes sense since we do need to track when the node is actually rendered. That's too bad, but I don't think harmful per se. |
No description provided.